package eval.cm.xml.parser.simple.parser.impl;

import java.util.Stack;

import eval.cm.xml.parser.simple.Document;
import eval.cm.xml.parser.simple.Element;
import static eval.cm.xml.parser.simple.parser.impl.StateMachineParser.State.*;
import static eval.cm.xml.parser.simple.parser.util.ParserUtils.*;

import eval.cm.xml.parser.simple.parser.Parser;
import eval.cm.xml.parser.simple.parser.XmlParsingException;

/**
 * This implementation of {@link Parser} uses the state machine(SM) approach.
 * We defined a finite set of states in which our SM can be at any point of time.
 * The input to our state machine represents one character at a time.
 * Depending of the character we decide the next state.
 * <br/>
 * We will *not* throw an exception if both an element value and
 * sub elements are present.
 * 
 * @author constantin
 *
 */
public class StateMachineParser implements Parser {
	
	public static enum State{
		/**
		 * We're outside of any tag.
		 * This is the initial state.
		 * No char has been consumed.
		 */
		OUTSIDE,
		
		/**
		 * A '&lt;' character is encountered.
		 * That means either a opening(e.g &lt;person&gt;) or a block closing(e.g &lt;/person&gt;) tag follows
		 */
		TAG_OPENING,
		
		/**
		 * A '/' character is encountered while the state machine is in {@link TAG_OPENING} state
		 */
		CLOSING_BLOCK_TAG,
		
		/**
		 * A '/' character is encountered while the state machine is in {@link IN_OPENING_TAG} state.
		 * That means an empty tag (e.g. &lt;person/&gt;)
		 */
		CLOSING_INLINE_TAG,
		
		/**
		 * We're inside the opening tag
		 */
		IN_OPENING_TAG,
		
		/**
		 * We're inside block closing tag(e.g &lt;/person&gt;)
		 */
		IN_BLOCK_CLOSING_TAG,
		
		/**
		 * We're inside a tag block
		 */
		INSIDE_TAG_BLOCK,
		
		/**
		 * We're inside a tag value (no sub elements are accepted)
		 */
		INSIDE_TAG_VALUE,
		
		/**
		 * The root element has been parsed correctly.
		 * Nothing more is expected
		 */
		END_DOCUMENT;
	};

	public Document parse( final String xmlContent )
		throws XmlParsingException
	{
		
		State state = OUTSIDE;
		StringBuilder tagNameOrValueBuff = new StringBuilder();
		StringBuilder whiteSpaceBuff = new StringBuilder();
		Stack<String> tags = new Stack<String>();
		Stack<Element> elements = new Stack<Element>();
		Element rootElement = null;
		
		int openingTagPosition = 0;	//holding the opening tag position for better exception handling
		char previousToken = (char)-1;//we use a reference to the previous token
			//to eliminate some states that we might need
		
		for( int i = 0, n = xmlContent.length(); i < n; i++ ) {
			final char token = xmlContent.charAt(i);
			switch (state) {
			case OUTSIDE: {
				if( Character.isWhitespace(token) ) {
					//ignoring leading spaces
					break;
				} else if( '<' != token ) {
					throw new XmlParsingException(
							"Xml document needs to start with an xml element");
				} else {
					openingTagPosition = i;
					state = TAG_OPENING;
				}
				break;
			}
			case INSIDE_TAG_BLOCK: {
				
				if( Character.isWhitespace(token) ) {
					//ignoring leading spaces
					break;
				} else if( '<' == token ) {
					//We have sub tags
					openingTagPosition = i;
					state = TAG_OPENING;
				} else if( isXmlTagValueAcceptedCharacter(token) ) {
					tagNameOrValueBuff.append(token);
					state = INSIDE_TAG_VALUE;
				} else {
					throw new XmlParsingException(
							String.format(
									"Invalid character encountered in the tag value at position %d: %c", i, token)
							 );
				}
				break;
			}
			case INSIDE_TAG_VALUE: {
				
				if( '<' == token ) {
					
					//We have ending tag
					Element element = elements.peek();
					element.setValue(tagNameOrValueBuff.toString());
					whiteSpaceBuff.delete(0, whiteSpaceBuff.length());
					tagNameOrValueBuff.delete(0, tagNameOrValueBuff.length());
					openingTagPosition = i;
					state = TAG_OPENING;
					
				} else if( isXmlTagValueAcceptedCharacter( token ) ) {
					if( Character.isWhitespace(token) ) {
						//We keep the whitespaces here in order not to count them at the end of the value
						whiteSpaceBuff.append(token);
					} else {
						tagNameOrValueBuff.append(whiteSpaceBuff);
						whiteSpaceBuff.delete(0, whiteSpaceBuff.length());
						tagNameOrValueBuff.append(token);
					}
				} else {
					throw new XmlParsingException(
							String.format(
									"Invalid character encountered in the tag value at position %d: %c", i, token)
							 );
				}
				break;
			}
			case TAG_OPENING: {
				if( Character.isLetter( token ) || '_' == token ) {
					//The tag name needs to start with a letter or underscore
					tagNameOrValueBuff.append(token);
					state = IN_OPENING_TAG;
				} else if ( '/' == token ) {
					state = CLOSING_BLOCK_TAG; 
				} else if( Character.isWhitespace( token ) ) {
					throw new XmlParsingException(
						"White spaces are not allowed right after tag opening");
				} else {
					throw new XmlParsingException(
						"Invalid tag name: " + xmlContent.substring( openingTagPosition, i ));
				}
				break;
			}
			case CLOSING_BLOCK_TAG: {
				if( Character.isLetter( token ) || '_' == token ) {
					//The tag name needs to start with a letter or underscore
					tagNameOrValueBuff.append(token);
					state = IN_BLOCK_CLOSING_TAG;
				} else if( Character.isWhitespace( token ) ) {
					throw new XmlParsingException(
						"White spaces are not allowed right after tag opening");
				} else {
					throw new XmlParsingException(
						"Invalid tag name: " + xmlContent.substring(openingTagPosition, i));
				}
				break;
			}
			case IN_OPENING_TAG:
			case IN_BLOCK_CLOSING_TAG: {
				if(
					Character.isLetterOrDigit( token ) ||
					isAny( token, '_', '.', '-' )
				) {
					if( Character.isWhitespace( previousToken ) ) {
						throw new XmlParsingException(
							"White spaces are not allowed in the tag name");
					} else {
						tagNameOrValueBuff.append(token);
					}
				} else if( Character.isWhitespace( token ) ) {
					//Ignoring trailing spaces of the tag name
				} else if( '>' == token ) {
					if( IN_OPENING_TAG == state ) {
						String tagName = tagNameOrValueBuff.toString();
						tagNameOrValueBuff.delete( 0, tagNameOrValueBuff.length() );
						tags.push( tagName );
						Element element = new Element(tagName);
						if( null == rootElement ) {
							rootElement = element;
						} else {
							Element parentElement = elements.peek();
							parentElement.appendChild(element);
						}
						elements.push(element);
						state = INSIDE_TAG_BLOCK;
					} else {
						//we're in IN_BLOCK_CLOSING_TAG state
						String tagName = tagNameOrValueBuff.toString();
						tagNameOrValueBuff.delete( 0, tagNameOrValueBuff.length() );
						if( tags.isEmpty() ) {
							throw new XmlParsingException(
									String.format( "A tag named %s was not previously opened", tagName ) );
						}
						String openTagName = tags.pop();
						elements.pop();
						if(tags.isEmpty()) {
							state = END_DOCUMENT;
						} else {
							state = INSIDE_TAG_BLOCK;
						}
						if( !openTagName.equals(tagName) ) {
							throw new XmlParsingException(
									String.format("Expected ending tag named %s", openTagName) );
						}
					}
				} else if( '/' == token ) {
					state = CLOSING_INLINE_TAG;
				} else {
					throw new XmlParsingException(
							String.format(
									"Invalid character encountered at position %d: %c", i, token)
							 );
				}
				break;
			}
			case CLOSING_INLINE_TAG: {
				if( '>' == token ) {
					String tagName = tagNameOrValueBuff.toString();
					Element element = new Element(tagName);
					if( null == rootElement ) {
						rootElement = element;	//this is a document with an empty tag
						state = END_DOCUMENT;
					} else {
						Element parentElement = elements.peek();
						parentElement.appendChild(element);
						state = INSIDE_TAG_BLOCK;
					}
				} else {
					throw new XmlParsingException(
							String.format(
									"Unexpected character encountered at position %d: %c. Expected '>'", i, token)
							 );
				}
				break;
			}
			case END_DOCUMENT: {
				if(!Character.isWhitespace(token)) {
					throw new XmlParsingException(
							String.format(
									"Unexpected character found after root element: %c", token)
							 );
				}
				break;
			}
			}
			previousToken = token;
		}
		if( state != END_DOCUMENT ) {
			throw new XmlParsingException(
					"Xml document needs to start with an xml element");
		}
		
		return new Document(rootElement);
	}

}
